-
Notifications
You must be signed in to change notification settings - Fork 59
fix(preview): Fix constant layout shift as preview rerenders #389
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
✅ Deploy Preview for stacks-editor ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
|
One regression I notice with this change is that we can end up with multiple spinners created by Screen.Recording.2025-03-04.at.10.14.35.movMaybe that was why there was this code for clearing out the preview in the first place? I'm not sure if that means there is something to be fixed in |
Renderers are async functions: in the current implementation once we call them there is no way to cancel the side effects that are tasked to do from the editor codebase unfortunately. What we can do though is to pass in an AbortController signal as option that the consumers implementing the renderer can leverage to abort whatever they are doing. This is how the update preview method will change (notice the addition of a new abortController class member) private updatePreview(text: string) {
this.container.innerHTML = "";
// if showing the preview, fire off the renderer async
if (this.isShown) {
// PART OF CODE REMOVED BY THIS PR
this.container.appendChild(this.dom);
// Abort any previous render
if (this.abortController) {
this.abortController.abort();
}
// Create a new AbortController for the current render
this.abortController = new AbortController();
const signal = this.abortController.signal;
void this.renderer?.(text, this.dom, { signal }).catch((e) => {
if (e.name !== 'AbortError') {
error(
"PreviewView.updatePreview",
`Uncaught exception in preview renderer`,
e
);
}
});
}
}Then on the consumer end you can consume the signal. In the case of our const interruptableSleepAsync = (delayMs, signal) => new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
resolve();
}, delayMs);
signal.addEventListener("abort", () => {
clearTimeout(timeoutId);
reject(new Error('aborted'));
});
});
interruptableSleepAsync(500, signal)
.then(() => {
// do what we were doing before
})
.catch(() => {
// cleanup any lingering dom elements added previously if necessary (e.g. remove the spinner)
})Now you need to consider that in Core the renderer is different and a bit more sophisticated / legacy. I am tagging @b-kelly since I don't know why we don't have already a mechanism to instruct consumers preview renderers to interrupt in-flight rendering. Perhaps we were relying on the fact that race conditions for async renders fired one after the other would be unlikely. |
|
@giamir That looks like a nice pattern to interrupt the renderer, I will work on implementing that |
|
Here's what the preview pane looks like with the AbortController at work @giamir @alexwarren screen-capture.2.webm |
|
@alizaberger before investing time on implementing the mechanism to interrupt in-flight rendering it would be interesting to know if that would be useful in our Core context in my opinion. You could go in Core to |
I think that's what I would expect based on how the examplePreviewRenderer is implemented in the demo site. We should not based our decision though on what that renderer but rather on what the renderer of Core does. This is why I was suggesting to check how your small changes behave in the Core context. If it does its job and doesn't have strange spinner appearing I would argue to keep the initial changes you made and adjust the implementation of the examplePreviewRenderer of the demo site so that we don't get that spinner cascade (perhaps we could remove the spinner all together from the renderer implementation). Hope my suggestion makes sense. If not feel free to DM me. :) |
This reverts commit b1236e5.
|
Final screen capture: screen-capture.3.webm |
dancormier
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR @alizaberger! I tested this change both in the standalone editor and by duplicating this change the editor in Core. Everything worked as I expected 🎉
My only (optional) suggestion is adding a test for this functionality. I'm fine with seeing this PR merged in as-is, but I think it could be worth trying to add tests to any code that's touched that doesn't already include a test case. Since this change is so minor, I'm ok with it not getting a test at the moment, but if you have a cycle or two to try an add one, I think it would be handy to have added.

Describe your changes
Fixes the issue described in this meta post. As the user types, preview container content is removed before the promise is resolved, causing the page layout to shift during the time it takes for the promise to resolve. The fix is to not clear the preview, rather wait for the content to be replaced when the promise is resolved.
Before
screen-capture.1.webm
After
screen-capture.webm
PR Checklist
/** ... */docsbug/enhancementand other labels as appropriateEnvironment(s) tested
Additional context
Add any other context about the PR here.